package com.region.spring.cloud.core.configure;

import com.region.common.adapter.logging.LoggerAdapter;
import com.region.common.adapter.logging.LoggerAdapterFactory;
import com.region.common.utils.ClassUtils;
import com.region.common.utils.StringUtils;
import com.region.core.env.ExpressionPropertiesOperator;
import com.region.core.env.Operators;
import com.region.core.env.PropertiesOperator;
import com.region.http.client.config.CustomClientConfigurer;
import com.region.http.client.core.CustomConfigurerRegister;
import com.region.http.client.model.CustomRequestMeta;
import com.region.loadbalancer.group.GroupServer;
import com.region.loadbalancer.group.Server;
import com.region.loadbalancer.listener.ConnectionListener;
import com.region.loadbalancer.monitor.ServerMonitor;
import com.region.loadbalancer.monitor.stat.DetectionConnection;
import com.region.loadbalancer.policy.BalancerPolicy;
import com.region.spring.cloud.core.RegionCloudAdapterProperties;
import com.region.spring.cloud.core.event.SpringCloudEvent;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author liujieyu
 * @date 2024/10/18 16:47
 * @desciption
 */
public class SpringCloudCustomConfigurer implements CustomClientConfigurer, DisposableBean {

    private static final LoggerAdapter loggerAdapter = LoggerAdapterFactory.getLogger(SpringCloudCustomConfigurer.class);

    private static final String DEFAULT_PROTOCOL = "https://";

    private final DiscoveryClient discoveryClient;

    // Saves whether the service generates a server group
    private final Map<String, Boolean> serviceInstances = new ConcurrentHashMap<>();

    // Saves service groups that are not found on the discovery center
    private final Map<String, List<CustomRequestMeta>> deadLetterMap = new ConcurrentHashMap<>();

    private RegionCloudAdapterProperties properties;

    private List<ConnectionListener> listeners;

    private CustomConfigurerRegister customConfigurerRegister;

    private ScheduledExecutorService executorService;

    private PropertiesOperator operator;

    public SpringCloudCustomConfigurer(DiscoveryClient discoveryClient, RegionCloudAdapterProperties properties, List<ConnectionListener> listeners) {
        this.discoveryClient = discoveryClient;
        this.properties = properties;
        this.listeners = listeners;
        this.executorService = defaultScheduleExecutor();
    }

    @Override
    public void addGroupServer(CustomConfigurerRegister configurerRegister) {
        List<String> serviceIds = this.discoveryClient.getServices();
        this.customConfigurerRegister = configurerRegister;
        for (int i = 0; i < serviceIds.size(); i++) {
            String serviceId = serviceIds.get(i);
            List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
            if (instances == null || instances.isEmpty()) {
                continue;
            }
            serviceInstances.put(serviceId, Boolean.FALSE);
        }
        // schedule refresh discovery center service id
        startScheduleRefreshDiscoveryClient(configurerRegister);
   }

    @Override
    public void configureCustomRequestMeta(CustomRequestMeta customRequestMeta) {
        String cloudUrl = customRequestMeta.getOriginUrl();
        String hostname;
        try {
            // auto compatible no protocol
            if (cloudUrl.indexOf("://") == -1) {
                cloudUrl = DEFAULT_PROTOCOL + cloudUrl;
            }
            URL url = new URL(replacePropertyUrl(cloudUrl));
            hostname = url.getHost();
            if (url.getPort() != -1) {
                hostname += ":" + url.getPort();
            }
            if(serviceInstances.containsKey(hostname)) {
                List<ServiceInstance> instances = this.discoveryClient.getInstances(hostname);
                if (instances == null || instances.isEmpty()) {
                    return;
                }
                // discovery client service id contain host
                customRequestMeta.setStatus(true);
                customRequestMeta.setGroupName(hostname);
                registerServer(customConfigurerRegister, hostname, instances);
            } else {
                if (deadLetterMap.get(hostname) == null) {
                    List<CustomRequestMeta> customMetaList = new ArrayList<>();
                    customMetaList.add(customRequestMeta);
                    deadLetterMap.put(hostname, customMetaList);
                } else {
                    deadLetterMap.get(hostname).add(customRequestMeta);
                }
                // has extension
                if (customRequestMeta.isExtension()) {
                    customRequestMeta.setStatus(true);
                    customRequestMeta.setGroupName(hostname);
                }
            }
        } catch (Exception e) {
            loggerAdapter.error("The spring cloud extension error", e);
        }
    }

    /**
     * Replaces the url after the environment variable in the source url
     *
     * @param originUrl
     * @return
     */
    private String replacePropertyUrl(String originUrl) {
        if (this.operator == null) {
            this.operator = Operators.createOperator(null);
        }
        ExpressionPropertiesOperator expressionPropertiesOperator = Operators.enableCacheExpressionOperator();
        List<String> list = expressionPropertiesOperator.parseExpression(originUrl);
        for (int i = 0; i < list.size(); i++) {
            originUrl = StringUtils.replace(originUrl,
                    PropertiesOperator.PREFIX_TAG + list.get(i) + PropertiesOperator.SUFFIX_TAG,
                    String.valueOf(operator.get(list.get(i))));
        }
        return originUrl;
    }

    private ScheduledExecutorService defaultScheduleExecutor() {
        return Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
            AtomicInteger threadNumber = new AtomicInteger(1);
            ThreadGroup group = (System.getSecurityManager() != null) ? System.getSecurityManager().getThreadGroup() :
                    Thread.currentThread().getThreadGroup();

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(group, r,
                        "Spring-Cloud-Region-Refresh-Scheduled-" + threadNumber.getAndIncrement(),
                        0);
                if (t.isDaemon())
                    t.setDaemon(false);
                if (t.getPriority() != Thread.NORM_PRIORITY)
                    t.setPriority(Thread.NORM_PRIORITY);
                return t;
            }
        });
    }

    /**
     * Start schedule refresh discovery client
     *
     * @param configurerRegister
     */
    private void startScheduleRefreshDiscoveryClient(CustomConfigurerRegister configurerRegister) {
        this.executorService.scheduleWithFixedDelay(() -> {
            List<String> services = this.discoveryClient.getServices();
            for (int i = 0; i < services.size(); i++) {
                String serviceId = services.get(i);
                if (!this.serviceInstances.containsKey(serviceId)) {
                    loggerAdapter.info("Instance service register to discovery, the service id is " + serviceId);
                    this.serviceInstances.put(serviceId, Boolean.FALSE);
                } else {
                    if (loggerAdapter.isDebugEnabled()) {
                        loggerAdapter.debug("The current instance service[" + serviceId + "] is registered.");
                    }
                }
                // handle dead better
                if (this.deadLetterMap.containsKey(serviceId)) {
                    List<CustomRequestMeta> customRequestMetas = this.deadLetterMap.get(serviceId);
                    for (int j = 0; j < customRequestMetas.size(); j++) {
                        CustomRequestMeta customRequestMeta = customRequestMetas.get(j);
                        List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
                        if (instances == null || instances.isEmpty()) {
                            return;
                        }
                        customRequestMeta.setStatus(true);
                        customRequestMeta.setGroupName(serviceId);
                        registerServer(configurerRegister, serviceId, instances);
                    }
                }
            }
        }, 0, this.properties.getRefreshTime(), TimeUnit.SECONDS);
    }

    /**
     * Register service instance to region load balancer
     */
    private void registerServer(CustomConfigurerRegister configurerRegister, String serviceId, List<ServiceInstance> instances) {
        if (this.serviceInstances.get(serviceId) == null || this.serviceInstances.get(serviceId).booleanValue()) {
            // not found or already registered service group
            return;
        }
        synchronized (this) {
            if (this.serviceInstances.get(serviceId) != null && !this.serviceInstances.get(serviceId).booleanValue()) {
                this.serviceInstances.put(serviceId, Boolean.TRUE);
                List<Server> servers = new ArrayList<>();
                for (ServiceInstance instance : instances) {
                    servers.add(Server.builder()
                            .protocol(instance.getScheme())
                            .host(instance.getHost())
                            .port(instance.getPort())
                            // How do I obtain a prime path; like if the instance server context path is "/server", so there prime path is "/server". If can do, to used @see com.region.spring.cloud.core.configure.SpringCloudDetectionConnection
                            .build());
                }
                try {
                    GroupServer groupServer = GroupServer
                            .builder()
                            .groupName(serviceId)
                            .autoDetect(properties.isAutoDetect())
                            .detectionConnection(ClassUtils.newInstance((Class<DetectionConnection>) ClassUtils.getClassLoader().loadClass(properties.getDetectionConnectionClassName())))
                            .policy(ClassUtils.newInstance((Class<BalancerPolicy>) ClassUtils.getClassLoader().loadClass(properties.getBalancerPolicyClassName())))
                            .detectOnBorrow(properties.isDetectOnBorrow())
                            .listeners(this.listeners)
                            .servers(servers)
                            .build();
                    configurerRegister.registerServer(groupServer);
                    // Registers the instance node status for periodic detection
                    ServerMonitor.getInstance().submitAsyncEvent(new SpringCloudEvent(serviceId, groupServer, discoveryClient));
                } catch (ClassNotFoundException e) {
                    loggerAdapter.error("Spring cloud custom configurer error.", e);
                }
            }
        }
    }

    @Override
    public void destroy() throws Exception {
        this.executorService.shutdownNow();
    }
}
